﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;

namespace FlowBasis.Data
{
    public static class LinqExpressionHelper
    {
        /// <summary>
        /// Attempt to get property name referenced for a LINQ expression of the form (o => o.Property). This
        /// will return null if it can't obtain a property name.
        /// </summary>
        /// <typeparam name="T">Type of the class containing the property.</typeparam>
        /// <param name="expression">LINQ expression of the form (o => o.Property)</param>
        /// <returns></returns>
        public static string TryGetReferencedPropertyName<T>(Expression<Func<T, object>> expression)
        {
            if (expression != null && expression.Body != null)
            {
                switch (expression.Body.NodeType)
                {
                    case ExpressionType.Convert:
                        {
                            var unaryExpression = expression.Body as UnaryExpression;
                            if (unaryExpression != null)
                            {
                                var memberExpression = unaryExpression.Operand as MemberExpression;
                                if (memberExpression != null)
                                {
                                    var propertyInfo = memberExpression.Member as PropertyInfo;
                                    if (propertyInfo != null)
                                    {
                                        return propertyInfo.Name;
                                    }
                                }
                            }
                            break;
                        }

                    case ExpressionType.MemberAccess:
                        {
                            var memberExpression = expression.Body as MemberExpression;
                            if (memberExpression != null)
                            {
                                var propertyInfo = memberExpression.Member as PropertyInfo;
                                if (propertyInfo != null)
                                {
                                    return propertyInfo.Name;
                                }
                            }
                            break;
                        }
                }
            }

            return null;
        }


        
        /// <summary>
        /// Create expression to match object set based on a property value.
        /// </summary>        
        /// <typeparam name="T">type of objects in IQueryable collection</typeparam>   
        /// <param name="propertyName">name of property to match</param>
        /// <param name="valueToMatch">value that property should have for a match</param>
        public static Expression<Func<T, bool>> CreatePropertyMatchLambdaExpression<T>(
            string propertyName,
            object valueToMatch)
        {            
            var linqParameter = Expression.Parameter(typeof(T), "obj");
            return Expression.Lambda<Func<T, bool>>(
                CreatePropertyMatchExpression<T>(linqParameter, propertyName, valueToMatch),
                linqParameter);
        }

        /// <summary>
        /// Create expression to match object set based on a property value.
        /// </summary>
        /// <typeparam name="T">type of objects in IQueryable collection</typeparam>   
        /// <param name="linqParameter">linqParameter be created with Expression.Parameter(typeof(T), "item")</param>
        /// <param name="propertyName">name of property to match</param>
        /// <param name="valueToMatch">value that property should have for a match</param>
        public static Expression CreatePropertyMatchExpression<T>(
            ParameterExpression linqParameter,
            string propertyName, 
            object valueToMatch)
        {      
            PropertyInfo property = typeof(T).GetProperty(propertyName);
            if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                // Special handling for nullable property.
                if (valueToMatch != null)
                {
                    var matchExpression =
                        Expression.AndAlso(
                            Expression.Equal(
                                Expression.Property(Expression.Property(linqParameter, propertyName), "HasValue"),
                                Expression.Constant(true)
                            ),
                            Expression.Equal(
                                Expression.Property(Expression.Property(linqParameter, propertyName), "Value"),
                                Expression.Constant(valueToMatch)
                            )
                        );

                    return matchExpression;
                }
                else
                {
                    var matchExpression =
                        Expression.Equal(
                            Expression.Property(Expression.Property(linqParameter, propertyName), "HasValue"),
                            Expression.Constant(false)
                        );

                    return matchExpression;
                }
            }
            else
            {
                var matchExpression =
                    Expression.Equal(
                        Expression.Property(linqParameter, propertyName),
                        Expression.Constant(valueToMatch)
                    );

                return matchExpression;
            }
        }


        /// <summary>
        /// Create lambda expression to match object set based on all given property values matching.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyNames"></param>
        /// <param name="propertyValues"></param>
        /// <returns></returns>
        public static Expression<Func<T, bool>> CreateMultiplePropertyMatchLambdaExpression<T>(
            IEnumerable<string> propertyNames,
            IEnumerable<object> propertyValues)
        {
            var linqParameter = Expression.Parameter(typeof(T), "obj");
            return Expression.Lambda<Func<T, bool>>(
                CreateMultiplePropertyMatchExpression<T>(linqParameter, propertyNames, propertyValues),
                linqParameter);
        }


        public static Expression CreateMultiplePropertyMatchExpression<T>(
            ParameterExpression linqParameter,
            IEnumerable<string> propertyNames,
            IEnumerable<object> propertyValues)
        {
            Expression andExpression = null;

            var valueEnumerator = propertyValues.GetEnumerator();
            foreach (string propertyName in propertyNames)
            {
                valueEnumerator.MoveNext();
                object propertyValue = valueEnumerator.Current;

                var matchExpression = CreatePropertyMatchExpression<T>(linqParameter, propertyName, propertyValue);
                if (andExpression == null)
                {
                    andExpression = matchExpression;
                }
                else
                {
                    andExpression = Expression.AndAlso(andExpression, matchExpression);
                }
            }

            return andExpression;
        }



        /// <summary>
        /// Create expression to match all objects with a property of a given name with value in list.
        /// </summary>
        /// <typeparam name="T">type of objects in IQueryable collection</typeparam>      
        /// <param name="propertyName">name of property to match</param>
        /// <param name="listOfValuesToMatch">list of values to match property against</param>
        /// <returns></returns>
        public static Expression<Func<T, bool>> CreatePropertyInListLamdaExpression<T>(            
            string propertyName,
            System.Collections.IList listOfValuesToMatch)
        {
            var linqParameter = Expression.Parameter(typeof(T), "obj");
            return Expression.Lambda<Func<T, bool>>(
                CreatePropertyInListExpression<T>(linqParameter, propertyName, listOfValuesToMatch),
                linqParameter);
        }


        /// <summary>
        /// Create expression to match all objects with a property of a given name with value in list.
        /// </summary>
        /// <typeparam name="T">type of objects in IQueryable collection</typeparam>
        /// <param name="linqParameter">linqParameter be created with Expression.Parameter(typeof(T), "item")</param>
        /// <param name="propertyName">name of property to match</param>
        /// <param name="listOfValuesToMatch">list of values to match property against</param>
        /// <returns></returns>
        public static MethodCallExpression CreatePropertyInListExpression<T>(
            ParameterExpression linqParameter,
            string propertyName,
            System.Collections.IList listOfValuesToMatch)
        {
            // Create expression which will find all entries with a property of a given name with value in list.            
            var containsMethodInfo = listOfValuesToMatch.GetType().GetMethod("Contains");

            var containsExpression = Expression.Call(
                System.Linq.Expressions.Expression.Constant(listOfValuesToMatch),
                containsMethodInfo,
                Expression.Property(linqParameter, propertyName));

            return containsExpression;
        }
    }
}
